/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.seari.utils.common;

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 *
 * @author Rainbow
 */
public class AnimatedGifEncoder
{

    protected int width; // image size
    protected int height;
    protected Color transparent=null; // transparent color if given
    protected int transIndex; // transparent index in color table
    protected int repeat=-1; // no repeat
    protected int delay=0; // frame delay (hundredths)
    protected boolean started=false; // ready to output frames
    protected OutputStream out;
    protected BufferedImage image; // current frame
    protected byte[] pixels; // BGR byte array from frame
    protected byte[] indexedPixels; // converted frame indexed to palette
    protected int colorDepth; // number of bit planes
    protected byte[] colorTab; // RGB palette
    protected boolean[] usedEntry=new boolean[256]; // active palette entries
    protected int palSize=7; // color table size (bits-1)
    protected int dispose=-1; // disposal code (-1 = use default)
    protected boolean closeStream=false; // close stream when finished
    protected boolean firstFrame=true;
    protected boolean sizeSet=false; // if false, get size from first frame
    protected int sample=10; // default sample interval for quantizer

    /**
     * Sets the delay time between each frame, or changes it for subsequent
     * frames (applies to last frame added).
     *
     * @param ms int delay time in milliseconds
     */
    public void setDelay(int ms)
    {
        delay=Math.round(ms/10.0f);
    }

    /**
     * Sets the GIF frame disposal code for the last added frame and any
     * subsequent frames. Default is 0 if no transparent color has been set,
     * otherwise 2.
     *
     * @param code int disposal code.
     */
    public void setDispose(int code)
    {
        if(code>=0)
        {
            dispose=code;
        }
    }

    /**
     * Sets the number of times the set of GIF frames should be played. Default
     * is 1; 0 means play indefinitely. Must be invoked before the first image
     * is added.
     *
     * @param iter int number of iterations.
     */
    public void setRepeat(int iter)
    {
        if(iter>=0)
        {
            repeat=iter;
        }
    }

    /**
     * Sets the transparent color for the last added frame and any subsequent
     * frames. Since all colors are subject to modification in the quantization
     * process, the color in the final palette for each frame closest to the
     * given color becomes the transparent color for that frame. May be set to
     * null to indicate no transparent color.
     *
     * @param c Color to be treated as transparent on display.
     */
    public void setTransparent(Color c)
    {
        transparent=c;
    }

    /**
     * Adds next GIF frame. The frame is not written immediately, but is
     * actually deferred until the next frame is received so that timing data
     * can be inserted. Invoking <code>finish()</code> flushes all frames. If
     * <code>setSize</code> was not invoked, the size of the first image is used
     * for all subsequent frames.
     *
     * @param im BufferedImage containing frame to write.
     * @return true if successful.
     */
    public boolean addFrame(BufferedImage im)
    {
        if((im==null)||!started)
        {
            return false;
        }
        boolean ok=true;
        try
        {
            if(!sizeSet)
            {
                // use first frame's size
                setSize(im.getWidth(), im.getHeight());
            }
            image=im;
            getImagePixels(); // convert to correct format if necessary
            analyzePixels(); // build color table & map pixels
            if(firstFrame)
            {
                writeLSD(); // logical screen descriptior
                writePalette(); // global color table
                if(repeat>=0)
                {
                    // use NS app extension to indicate reps
                    writeNetscapeExt();
                }
            }
            writeGraphicCtrlExt(); // write graphic control extension
            writeImageDesc(); // image descriptor
            if(!firstFrame)
            {
                writePalette(); // local color table
            }
            writePixels(); // encode and write pixel data
            firstFrame=false;
        }
        catch(IOException e)
        {
            ok=false;
        }

        return ok;
    }

    /**
     * Flushes any pending data and closes output file. If writing to an
     * OutputStream, the stream is not closed.
     */
    public boolean finish()
    {
        if(!started)
            return false;
        boolean ok=true;
        started=false;
        try
        {
            out.write(0x3b); // gif trailer
            out.flush();
            if(closeStream)
            {
                out.close();
            }
        }
        catch(IOException e)
        {
            ok=false;
        }

        // reset for subsequent use
        transIndex=0;
        out=null;
        image=null;
        pixels=null;
        indexedPixels=null;
        colorTab=null;
        closeStream=false;
        firstFrame=true;

        return ok;
    }

    /**
     * Sets frame rate in frames per second. Equivalent to
     * <code>setDelay(1000/fps)</code>.
     *
     * @param fps float frame rate (frames per second)
     */
    public void setFrameRate(float fps)
    {
        if(fps!=0f)
        {
            delay=Math.round(100f/fps);
        }
    }

    /**
     * Sets quality of color quantization (conversion of images to the maximum
     * 256 colors allowed by the GIF specification). Lower values (minimum = 1)
     * produce better colors, but slow processing significantly. 10 is the
     * default, and produces good color mapping at reasonable speeds. Values
     * greater than 20 do not yield significant improvements in speed.
     *
     * @param quality int greater than 0.
     */
    public void setQuality(int quality)
    {
        if(quality<1)
            quality=1;
        sample=quality;
    }

    /**
     * Sets the GIF frame size. The default size is the size of the first frame
     * added if this method is not invoked.
     *
     * @param w int frame width.
     * @param h int frame width.
     */
    public void setSize(int w, int h)
    {
        if(started&&!firstFrame)
            return;
        width=w;
        height=h;
        if(width<1)
            width=320;
        if(height<1)
            height=240;
        sizeSet=true;
    }

    /**
     * Initiates GIF file creation on the given stream. The stream is not closed
     * automatically.
     *
     * @param os OutputStream on which GIF images are written.
     * @return false if initial write failed.
     */
    public boolean start(OutputStream os)
    {
        if(os==null)
            return false;
        boolean ok=true;
        closeStream=false;
        out=os;
        try
        {
            writeString("GIF89a"); // header
        }
        catch(IOException e)
        {
            ok=false;
        }
        return started=ok;
    }

    /**
     * Initiates writing of a GIF file with the specified name.
     *
     * @param file String containing output file name.
     * @return false if open or initial write failed.
     */
    public boolean start(String file)
    {
        boolean ok=true;
        try
        {
            out=new BufferedOutputStream(new FileOutputStream(file));
            ok=start(out);
            closeStream=true;
        }
        catch(IOException e)
        {
            ok=false;
        }
        return started=ok;
    }

    /**
     * Analyzes image colors and creates color map.
     */
    protected void analyzePixels()
    {
        int len=pixels.length;
        int nPix=len/3;
        indexedPixels=new byte[nPix];
        NeuQuant nq=new NeuQuant(pixels, len, sample);
        // initialize quantizer
        colorTab=nq.process(); // create reduced palette
        // convert map from BGR to RGB
        for(int i=0; i<colorTab.length; i+=3)
        {
            byte temp=colorTab[i];
            colorTab[i]=colorTab[i+2];
            colorTab[i+2]=temp;
            usedEntry[i/3]=false;
        }
        // map image pixels to new palette
        int k=0;
        for(int i=0; i<nPix; i++)
        {
            int index
                    =nq.map(pixels[k++]&0xff,
                            pixels[k++]&0xff,
                            pixels[k++]&0xff);
            usedEntry[index]=true;
            indexedPixels[i]=(byte)index;
        }
        pixels=null;
        colorDepth=8;
        palSize=7;
        // get closest match to transparent color if specified
        if(transparent!=null)
        {
            transIndex=findClosest(transparent);
        }
    }

    /**
     * Returns index of palette color closest to c
     *
     */
    protected int findClosest(Color c)
    {
        if(colorTab==null)
            return -1;
        int r=c.getRed();
        int g=c.getGreen();
        int b=c.getBlue();
        int minpos=0;
        int dmin=256*256*256;
        int len=colorTab.length;
        for(int i=0; i<len;)
        {
            int dr=r-(colorTab[i++]&0xff);
            int dg=g-(colorTab[i++]&0xff);
            int db=b-(colorTab[i]&0xff);
            int d=dr*dr+dg*dg+db*db;
            int index=i/3;
            if(usedEntry[index]&&(d<dmin))
            {
                dmin=d;
                minpos=index;
            }
            i++;
        }
        return minpos;
    }

    /**
     * Extracts image pixels into byte array "pixels"
     */
    protected void getImagePixels()
    {
        int w=image.getWidth();
        int h=image.getHeight();
        int type=image.getType();
        if((w!=width)
                ||(h!=height)
                ||(type!=BufferedImage.TYPE_3BYTE_BGR))
        {
            // create new image with right size/format
            BufferedImage temp
                    =new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
            Graphics2D g=temp.createGraphics();
            g.drawImage(image, 0, 0, null);
            image=temp;
        }
        pixels=((DataBufferByte)image.getRaster().getDataBuffer()).getData();
    }

    /**
     * Writes Graphic Control Extension
     */
    protected void writeGraphicCtrlExt() throws IOException
    {
        out.write(0x21); // extension introducer
        out.write(0xf9); // GCE label
        out.write(4); // data block size
        int transp, disp;
        if(transparent==null)
        {
            transp=0;
            disp=0; // dispose = no action
        }
        else
        {
            transp=1;
            disp=2; // force clear if using transparent color
        }
        if(dispose>=0)
        {
            disp=dispose&7; // user override
        }
        disp<<=2;

        // packed fields
        out.write(0| // 1:3 reserved
                disp| // 4:6 disposal
                0| // 7   user input - 0 = none
                transp); // 8   transparency flag

        writeShort(delay); // delay x 1/100 sec
        out.write(transIndex); // transparent color index
        out.write(0); // block terminator
    }

    /**
     * Writes Image Descriptor
     */
    protected void writeImageDesc() throws IOException
    {
        out.write(0x2c); // image separator
        writeShort(0); // image position x,y = 0,0
        writeShort(0);
        writeShort(width); // image size
        writeShort(height);
        // packed fields
        if(firstFrame)
        {
            // no LCT  - GCT is used for first (or only) frame
            out.write(0);
        }
        else
        {
            // specify normal LCT
            out.write(0x80| // 1 local color table  1=yes
                    0| // 2 interlace - 0=no
                    0| // 3 sorted - 0=no
                    0| // 4-5 reserved
                    palSize); // 6-8 size of color table
        }
    }

    /**
     * Writes Logical Screen Descriptor
     */
    protected void writeLSD() throws IOException
    {
        // logical screen size
        writeShort(width);
        writeShort(height);
        // packed fields
        out.write((0x80| // 1   : global color table flag = 1 (gct used)
                0x70| // 2-4 : color resolution = 7
                0x00| // 5   : gct sort flag = 0
                palSize)); // 6-8 : gct size

        out.write(0); // background color index
        out.write(0); // pixel aspect ratio - assume 1:1
    }

    /**
     * Writes Netscape application extension to define repeat count.
     */
    protected void writeNetscapeExt() throws IOException
    {
        out.write(0x21); // extension introducer
        out.write(0xff); // app extension label
        out.write(11); // block size
        writeString("NETSCAPE"+"2.0"); // app id + auth code
        out.write(3); // sub-block size
        out.write(1); // loop sub-block id
        writeShort(repeat); // loop count (extra iterations, 0=repeat forever)
        out.write(0); // block terminator
    }

    /**
     * Writes color table
     */
    protected void writePalette() throws IOException
    {
        out.write(colorTab, 0, colorTab.length);
        int n=(3*256)-colorTab.length;
        for(int i=0; i<n; i++)
        {
            out.write(0);
        }
    }

    /**
     * Encodes and writes pixel data
     */
    protected void writePixels() throws IOException
    {
        LZWEncoder encoder=new LZWEncoder(width, height, indexedPixels, colorDepth);
        encoder.encode(out);
    }

    /**
     * Write 16-bit value to output stream, LSB first
     */
    protected void writeShort(int value) throws IOException
    {
        out.write(value&0xff);
        out.write((value>>8)&0xff);
    }

    /**
     * Writes string to output stream
     */
    protected void writeString(String s) throws IOException
    {
        for(int i=0; i<s.length(); i++)
        {
            out.write((byte)s.charAt(i));
        }
    }
}
